package jxtend.processor.eclipse;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import jxtend.processor.eclipse.visitor.handler.impl.ASTNodeBits;
import jxtend.processor.exception.JxtendProcessorException;
import jxtend.processor.reflect.ReflectField;
import jxtend.processor.utils.ReflectUtils;

import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.ast.Clinit;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Initializer;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.eclipse.jdt.internal.compiler.lookup.ImportBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodScope;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.util.CompoundNameVector;
import org.eclipse.jdt.internal.compiler.util.HashtableOfType;
import org.eclipse.jdt.internal.compiler.util.ObjectVector;
import org.eclipse.jdt.internal.compiler.util.SimpleNameVector;
import org.eclipse.jdt.internal.eval.CodeSnippetScope;

@SuppressWarnings("restriction")
public final class ScopeUtils {
	private static final Field qualifiedReferencesField =
		ReflectUtils.getInaccessibleField(CompilationUnitScope.class, "qualifiedReferences"); 
	private static final Field simpleNameReferencesField =
		ReflectUtils.getInaccessibleField(CompilationUnitScope.class, "simpleNameReferences"); 
	private static final Field rootReferencesField =
		ReflectUtils.getInaccessibleField(CompilationUnitScope.class, "rootReferences"); 
	private static final Field referencedTypesField =
		ReflectUtils.getInaccessibleField(CompilationUnitScope.class, "referencedTypes"); 
	private static final Field referencedSuperTypesField =
		ReflectUtils.getInaccessibleField(CompilationUnitScope.class, "referencedSuperTypes"); 
	private static final Field constantPoolNameUsageField =
		ReflectUtils.getInaccessibleField(CompilationUnitScope.class, "constantPoolNameUsage"); 
	private static final Field captureIDField =
		ReflectUtils.getInaccessibleField(CompilationUnitScope.class, "captureID");
	
	private static final Constructor<BlockScope> blockScopeConstructor =
		ReflectUtils.getInaccessibleConstructor(BlockScope.class, int.class, Scope.class);
	private static final Constructor<CodeSnippetScope> codeSnippetScopeConstructor =
			ReflectUtils.getInaccessibleConstructor(CodeSnippetScope.class, int.class, Scope.class);

	private static final @SuppressWarnings("rawtypes") ReflectField<ClassScope, ArrayList> deferredBoundChecksOfClassScope =
			ReflectField.getField(ClassScope.class, ArrayList.class, "deferredBoundChecks");

	public static Map<Scope, Scope> backupBlockScopes(CompilationUnitDeclaration compilationUnit) {
		final Map<Scope, Scope> result = new HashMap<Scope, Scope>();

		compilationUnit.traverse(new BlockScopeVisitor() {
			@Override
			public void visitBlockScope(BlockScope blockScope) {
				result.put(blockScope, cloneBlockScope(blockScope));
			}
		}, compilationUnit.scope);

		
		CompilationUnitScope originalCompilationUnitScope = compilationUnit.scope;
		CompilationUnitScope clonedCompilationUnitScope = cloneCompilationUnitScope(compilationUnit.scope);
		compilationUnit.scope = originalCompilationUnitScope;
		
		result.put(compilationUnit.scope, clonedCompilationUnitScope);
	
		return result;
	}

	public static Scope cloneScope(Scope scope) {
		if (scope == null) {
			return null;
		}

		if (scope instanceof CompilationUnitScope) {
			return cloneCompilationUnitScope((CompilationUnitScope) scope);
		} else if (scope instanceof ClassScope) {
			return cloneClassScope((ClassScope) scope);
		} else if (scope instanceof BlockScope) {
			return cloneBlockScope((BlockScope) scope);
		} else {
			throw new IllegalArgumentException("Invalid scope type: " + scope.getClass().getName());
		}
	}
	
	private static CompilationUnitScope cloneCompilationUnitScope(CompilationUnitScope compilationUnitScope) {
		if (compilationUnitScope == null) {
			return null;
		}

		CompilationUnitScope result =
			new CompilationUnitScope(compilationUnitScope.referenceContext, compilationUnitScope.environment);
		copyCompilationUnitScopeProperties(compilationUnitScope, result);

		return result;
	}

	private static void copyCompilationUnitScopeProperties(CompilationUnitScope source, CompilationUnitScope target) {
		try {
			target.environment = source.environment;
			target.referenceContext = source.referenceContext;
			target.currentPackageName = cloneArray(source.currentPackageName);
			target.fPackage = source.fPackage;

			target.imports = source.imports;
			target.typeOrPackageCache = EcjCollectionUtils.clone(source.typeOrPackageCache);

			target.topLevelTypes = source.topLevelTypes;
			qualifiedReferencesField.set(target,
					EcjCollectionUtils.clone((CompoundNameVector) qualifiedReferencesField.get(source)));
			simpleNameReferencesField.set(target,
					EcjCollectionUtils.clone((SimpleNameVector) simpleNameReferencesField.get(source)));
			rootReferencesField.set(target,
					EcjCollectionUtils.clone((SimpleNameVector) rootReferencesField.get(source)));
			referencedTypesField.set(target,
					EcjCollectionUtils.clone((ObjectVector) referencedTypesField.get(source)));
			referencedSuperTypesField.set(target,
					EcjCollectionUtils.clone((ObjectVector) referencedSuperTypesField.get(source)));
			constantPoolNameUsageField.set(target,
					EcjCollectionUtils.clone((HashtableOfType) constantPoolNameUsageField.get(source)));
			captureIDField.set(target, (Integer) captureIDField.get(source));
		} catch (IllegalAccessException e) {
			throw new JxtendProcessorException(e);
		}
	}

	private static ClassScope cloneClassScope(ClassScope classScope) {
		ClassScope result = new ClassScope(classScope.parent, classScope.referenceContext);
		copyClassScopeProperties(classScope, result);
		return result;
	}
	
	private static BlockScope cloneBlockScope(BlockScope blockScope) {
		BlockScope result;
		
		if (blockScope instanceof MethodScope) {
			result = new MethodScope((ClassScope) blockScope.parent,
					((MethodScope) blockScope).referenceContext, ((MethodScope) blockScope).isStatic);
		} else {
			result = (blockScope instanceof CodeSnippetScope)
					? createCodeSnippetScope(blockScope.kind, blockScope.parent)
					: createBlockScope(blockScope.kind, blockScope.parent);
		}

		copyBlockScopeProperties(blockScope, result, true);
		return result;
	}

	private static BlockScope createBlockScope(int kind, Scope parent) {
		return ReflectUtils.newInstance(blockScopeConstructor, kind, parent);
	}

	private static CodeSnippetScope createCodeSnippetScope(int kind, Scope parent) {
		return ReflectUtils.newInstance(codeSnippetScopeConstructor, kind, parent);
	}

	public static void restoreBlockScopes(
			CompilationUnitDeclaration compilationUnit, final Map<Scope, Scope> backuppedScopes) {
		CompilationUnitScope backuppedCompilationUnitScope =
			(CompilationUnitScope) backuppedScopes.get(compilationUnit.scope);

		Set<ImportBinding> generatedImportBindings = new HashSet<ImportBinding>();
		if (compilationUnit.scope.imports != null) {
			for (ImportBinding importBinding : compilationUnit.scope.imports) {
				if (importBinding.reference != null) {
					importBinding.reference.bits |= org.eclipse.jdt.internal.compiler.ast.ASTNode.Used;
				}

				if ((importBinding.reference == null)
						|| ((importBinding.reference.bits & ASTNodeBits.JXTEND_GENERATED) == 0)) {
					continue;
				}

				generatedImportBindings.add(importBinding);
			}
		}
		
		if (backuppedCompilationUnitScope != null) {
			copyCompilationUnitScopeProperties(backuppedCompilationUnitScope, compilationUnit.scope);
		}
		if (compilationUnit.scope.typeOrPackageCache != null) {
			for (ImportBinding importBinding : generatedImportBindings) {
				compilationUnit.scope.typeOrPackageCache.put(
						importBinding.compoundName[importBinding.compoundName.length -1], importBinding);
			}
		}

		compilationUnit.traverse(new BlockScopeVisitor() {
			@Override
			public void visitBlockScope(BlockScope blockScope) {
				BlockScope backuppedBlockScope = (BlockScope) backuppedScopes.get(blockScope);

				copyProperties(backuppedBlockScope, blockScope);
			}
		}, compilationUnit.scope);
	}	

	private static abstract class BlockScopeVisitor extends ASTVisitor {
		public void endVisit(Clinit clinit, ClassScope scope) {
			invokeVisitBlockScope(clinit.scope);
		}

		public void endVisit(ConstructorDeclaration constructorDeclaration, ClassScope scope) {
			invokeVisitBlockScope(constructorDeclaration.scope);
		}

		public void endVisit(MethodDeclaration methodDeclaration, ClassScope scope) {
			invokeVisitBlockScope(methodDeclaration.scope);
		}

		public void endVisit(Initializer initializer, MethodScope scope) {
			invokeVisitBlockScope(initializer.block.scope);
		}

		private final void invokeVisitBlockScope(BlockScope blockScope) {
			if (blockScope == null) {
				return;
			}

			visitBlockScope(blockScope);
		}
		
		public abstract void visitBlockScope(BlockScope blockScope);	
	}

	public static void copyProperties(Scope source, Scope target) {
		if (source instanceof CompilationUnitScope) {
			copyCompilationUnitScopeProperties((CompilationUnitScope) source, (CompilationUnitScope) target); 
		} else if (source instanceof ClassScope) {
			copyClassScopeProperties((ClassScope) source, (ClassScope) target); 			
		} else if (source instanceof BlockScope) {
			if (source instanceof MethodScope) {
				copyBlockScopeProperties((BlockScope) source, (BlockScope) target, false);
			} else {
				copyBlockScopeProperties((BlockScope) source, (BlockScope) target, true);
			}
		}
	}

	private static void copyBlockScopeProperties(BlockScope source, BlockScope target, boolean copyLocals) {
		target.parent = source.parent;
		if (copyLocals) {
			target.locals = cloneArray(source.locals);
			target.localIndex = source.localIndex;
		}
		target.startIndex = source.startIndex;
		target.offset = source.offset;
		target.maxOffset = source.maxOffset;
		target.shiftScopes = cloneArray(source.shiftScopes);
		target.subscopes = cloneArray(source.subscopes);
		target.subscopeCount = source.subscopeCount;
		target.enclosingCase = source.enclosingCase;

		if (source instanceof MethodScope) {
			copyMethodScopeSpecificProperties((MethodScope) source, (MethodScope) target);
		}
	}

	private static void copyMethodScopeSpecificProperties(MethodScope source, MethodScope target) {
		target.parent = source.parent;
		target.referenceContext = source.referenceContext;
		target.isStatic = source.isStatic;
		target.isConstructorCall = source.isConstructorCall;
		target.initializedField = source.initializedField;
		target.lastVisibleFieldID = source.lastVisibleFieldID;
		target.analysisIndex = source.analysisIndex;
		target.isPropagatingInnerClassEmulation = source.isPropagatingInnerClassEmulation;
		target.lastIndex = source.lastIndex;
		target.definiteInits = cloneArray(source.definiteInits);
		target.extraDefiniteInits = cloneArray(source.extraDefiniteInits);
		target.insideTypeAnnotation = source.insideTypeAnnotation;
		target.extraSyntheticArguments = cloneArray(source.extraSyntheticArguments);
	}
	
	private static void copyClassScopeProperties(ClassScope source, ClassScope target) {
		target.parent = source.parent;
		target.referenceContext = source.referenceContext;
		target.superTypeReference = source.superTypeReference;

		ArrayList<?> deferredBoundChecksFromSource = deferredBoundChecksOfClassScope.get(source);
		deferredBoundChecksOfClassScope.set(target, (deferredBoundChecksFromSource != null)
					? (ArrayList<?>) ((ArrayList<?>) deferredBoundChecksFromSource).clone() : (ArrayList<?>) null);
	}

	private static long[] cloneArray(long[] array) {
		if (array == null) {
			return null;
		}
	
		return Arrays.copyOf(array, array.length);
	}

	private static long[][] cloneArray(long[][] array) {
		if (array == null) {
			return null;
		}
	
		long[][] result = new long[array.length][];
		for (int i = 0; i < result.length; i++) {
			result[i] = cloneArray(array[i]);
			
		}
		return result;
	}

	private static <T> T[] cloneArray(T[] array) {
		if (array == null) {
			return null;
		}
	
		return Arrays.copyOf(array, array.length);
	}	
}
